package cn.rocksolid.sport.tx;

import cn.rocksolid.sport.common.C;
import cn.rocksolid.sport.common.Constant.Symbol;
import cn.rocksolid.sport.common.Constant.TxSt;
import cn.rocksolid.sport.common.context.RockSolidContext;
import cn.rocksolid.sport.common.context.RockSolidContextHolder;
import cn.rocksolid.sport.common.entity.IdEntity;
import cn.rocksolid.sport.common.entity.TxEnable;
import cn.rocksolid.sport.tx.entity.TxTraceEntity;
import cn.rocksolid.sport.tx.service.TxTraceService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.IService;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionTimedOutException;

import java.lang.annotation.*;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.TimeoutException;

@Aspect
@Component
public class TxAop {

  @Target(ElementType.METHOD)
  @Retention(RetentionPolicy.RUNTIME)
  public @interface Tx {
    //    String[] allows()
  }

  @Target(ElementType.PARAMETER)
  @Retention(RetentionPolicy.RUNTIME)
  public @interface Txp {
    boolean value() default true;
  }

  private final TxTraceService txTraceService;

  @Autowired
  public TxAop(final TxTraceService txTraceService) {
    this.txTraceService = txTraceService;
  }

  @Pointcut("@annotation(cn.rocksolid.sport.tx.TxAop.Tx)")
  private void trace() {
    // there leave blank
  }

  private Map<String, Object> id(String type, Object value) {
    return ImmutableMap.of(C.TYPE.p, type, C.VALUE.p, ObjectUtils.defaultIfNull(value, Symbol.EMPTY));
  }

  private JSONObject collectSt(final ProceedingJoinPoint joinPoint, final List<String> tbr) {
    JSONObject sts = new JSONObject();
    if (CollectionUtils.isNotEmpty(tbr)) {
      @SuppressWarnings("unchecked")
      IService<TxEnable<?>> biz = (IService<TxEnable<?>>) joinPoint.getThis();
      List<TxEnable<?>> entities = biz.listByIds(tbr);
      for (TxEnable<?> ste : CollectionUtils.emptyIfNull(entities)) {
        sts.put(ste.getId(), ste.getSt());
      }
    }
    return sts;
  }

  private JSONArray collectExp(final Throwable t) {
    JSONArray exp = new JSONArray();
    exp.add(t.getClass().getName());
    exp.add(t.getMessage());
    return exp;
  }

  private void args(final Method m, final Object[] args, final JSONObject ids, final List<String> tbr) {
    Annotation[][] pas = m.getParameterAnnotations();
    for (int i = 0; i < pas.length; i++) {
      for (int j = 0; j < ArrayUtils.nullToEmpty(pas[i]).length; j++) {
        if (pas[i][j] instanceof Txp) {
          if (((Txp) pas[i][j]).value()) {
            ids.put(String.valueOf(i), id(C.ID.p, args[i]));
            tbr.add(String.valueOf(args[i]));
          } else if (args[i] instanceof IdEntity) {
            IdEntity idEntity = (IdEntity) args[i];
            ids.put(String.valueOf(i), id(C.ID_ENTITY.p, idEntity.getId()));
            tbr.add(idEntity.getId());
          } else if (args[i] instanceof Collection<?> && CollectionUtils.isNotEmpty((Collection<?>) args[i])) {
            int p = 0;
            for (Object obj : (Collection<?>) args[i]) {
              if (obj instanceof IdEntity) {
                IdEntity idEntity = (IdEntity) obj;
                ids.put(Symbol.EMPTY + i + Symbol.DOT + p, id(C.ID_ENTITY.p, idEntity.getId()));
                tbr.add(idEntity.getId());
              } else {
                ids.put(Symbol.EMPTY + i + Symbol.DOT + p, id(C.UNKNOWN.p, obj.getClass().getName()));
              }
              p++;
            }
          } else {
            ids.put(String.valueOf(i), id(C.UNKNOWN.p, args[i].getClass().getName()));
          }
        }
      }
    }
  }

  @Around("cn.rocksolid.sport.tx.TxAop.trace()")
  public Object recording(final ProceedingJoinPoint joinPoint) throws Throwable {
    Object[] args = joinPoint.getArgs();
    Method m = ((MethodSignature) joinPoint.getSignature()).getMethod();
    JSONObject ids = new JSONObject();
    List<String> tbr = new ArrayList<>();
    args(m, args, ids, tbr);

    RockSolidContext ctx = RockSolidContextHolder.getContext();
    TxTraceEntity txTraceEntity = new TxTraceEntity();
    txTraceEntity.setIds(ids);
    txTraceEntity.setClazz(m.getDeclaringClass().getName());
    txTraceEntity.setTx(m.getName());
    txTraceEntity.setOst(collectSt(joinPoint, tbr));
    txTraceEntity.setArgs(new JSONArray(Arrays.asList(args)));
    txTraceEntity.setAppId(ctx.getAppId());
    txTraceEntity.setAccessId(ctx.getAccessId());
    txTraceEntity.setAccessType(ctx.getAccessType());
    txTraceEntity.setClientIp(ctx.getClientIp());
    txTraceEntity.setLocation(ctx.getLocation());
    txTraceEntity.setStartDt(new Date());
    txTraceEntity.setSt(TxSt.APPLY);
    txTraceService.insert(txTraceEntity);
    try {
      Object r = joinPoint.proceed();
      if (null != r) {
        Object result = JSON.toJSON(r);
        if (result instanceof JSONArray) {
          txTraceEntity.setResult((JSONArray) result);
        } else {
          txTraceEntity.setResult(new JSONArray(ImmutableList.of(result)));
        }
      }
      txTraceEntity.setSt(TxSt.SUCCESS);
      return r;
    } catch (final TransactionTimedOutException | TimeoutException toe) {
      txTraceEntity.setSt(TxSt.TIMEOUT);
      txTraceEntity.setResult(collectExp(toe));
      throw toe;
    } catch (final Exception exp) {
      txTraceEntity.setSt(TxSt.FAILURE);
      txTraceEntity.setResult(collectExp(exp));
      throw exp;
    } finally {
      txTraceEntity.setFst(collectSt(joinPoint, tbr));
      txTraceEntity.setEndDt(new Date());
      txTraceService.update(txTraceEntity);
    }
  }
}
